Housecarl will be a firewall manager for nftables, built the way Jarl and Steward are: housecarld (a daemon with a REST API and web UI), hc (the CLI) and SQLite. - The logo: a carved Nordic doorway in Jarl's blue (artwork/logo.svg), with a small version without the carvings, lighter on dark backgrounds. scripts/logo.py builds the web UI's copies and scripts/banner.py the README banner. - The HOUSECARL wordmark in Jarl's and Steward's angular letters, with new H, O, U and C (assets/brand.js), and the UI's color tokens. - housecarld and hc print their version; the Makefile, version package, AGENTS.md, CHANGELOG and the design sketch (docs/design.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
4.1 KiB
JavaScript
73 lines
4.1 KiB
JavaScript
// The Housecarl logo and the login page's wordmark, drawn as inline SVG so
|
|
// they appear together with the page instead of loading afterwards.
|
|
//
|
|
// The "HOUSECARL" wordmark: the angular letters of Jarl's and Steward's
|
|
// wordmarks (A, R and L are Jarl's, S and E Steward's), with an H whose
|
|
// crossbar rises like the doorway's roof. Drawn as paths so no font is
|
|
// needed. The login page shows it over a rule with an FW node; the header
|
|
// shows the letters alone. The letters use the text color and the rule the
|
|
// accent color.
|
|
import * as logoData from "./logo.js";
|
|
|
|
const NS = "http://www.w3.org/2000/svg";
|
|
|
|
function el(tag, attrs, ...children) {
|
|
const e = document.createElementNS(NS, tag);
|
|
for (const [k, v] of Object.entries(attrs || {})) e.setAttribute(k, v);
|
|
for (const c of children) e.append(c instanceof Node ? c : document.createTextNode(c));
|
|
return e;
|
|
}
|
|
|
|
// Letters on a 100-unit cap height; counters are cut with evenodd.
|
|
const letters = [
|
|
// H: two stems; the crossbar rises to a peak, like the doorway's roof
|
|
{ x: 0, d: "M0 0 H18 V40 L32 32 L46 40 V0 H64 V100 H46 V58 L32 50 L18 58 V100 H0 Z" },
|
|
// O: an octagon, with the cut corners of R's bowl
|
|
{ x: 76, d: "M20 0 H44 L64 20 V80 L44 100 H20 L0 80 V20 Z M24 16 H40 L46 22 V78 L40 84 H24 L18 78 V22 Z" },
|
|
// U: cut corners at the foot
|
|
{ x: 152, d: "M0 0 H18 V78 L24 84 H40 L46 78 V0 H64 V80 L44 100 H20 L0 80 Z" },
|
|
// S and E: Steward's
|
|
{ x: 228, d: "M10 0 H54 L58 30 L40 16 H18 V41 H48 L58 51 V90 L48 100 H4 L0 70 L18 84 H40 V59 H10 L0 49 V10 Z" },
|
|
{ x: 298, d: "M0 0 H52 L56 18 H18 V41 H44 V59 H18 V82 H40 L58 70 L54 100 H0 Z" },
|
|
// C: S's downward barb at the top, E's upswept spike at the foot
|
|
{ x: 368, d: "M20 0 H56 L60 30 L42 16 H24 L18 22 V78 L24 84 H42 L60 70 L56 100 H20 L0 80 V20 Z" },
|
|
// A, R and L: Jarl's
|
|
{ x: 440, d: "M35 0 L46 10 L64 90 L70 100 H50 L44 76 L35 62 L26 76 L20 100 H0 L6 90 L24 10 Z M35 26 L41 40 L35 54 L29 40 Z" },
|
|
{ x: 522, d: "M0 0 H48 L60 12 V40 L50 50 L66 100 H46 L34 60 H18 V100 H0 Z M18 14 H41 L46 19 V35 L41 46 H18 Z" },
|
|
{ x: 600, d: "M0 0 H18 V84 H40 L58 70 L54 100 H0 Z" },
|
|
];
|
|
|
|
const W = 658; // the letters' width
|
|
|
|
// 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 ${W + 4} 104`, role: "img", "aria-label": "Housecarl" };
|
|
if (height) Object.assign(attrs, { height, width: Math.round(height * (W + 4) / 104) });
|
|
return el("svg", attrs, el("title", null, "Housecarl"), letterG);
|
|
}
|
|
const c = W / 2;
|
|
const ruleG = el("g", { class: "wordmark-rule", transform: "translate(0 118)" },
|
|
el("path", { d: `M2 18 L${c - 60} 15.6 V20.4 Z M${W - 2} 18 L${c + 60} 15.6 V20.4 Z`, class: "fill" }),
|
|
el("circle", { cx: c - 52, cy: 18, r: 4.6, class: "line" }),
|
|
el("path", { d: `M${c - 47.4} 18 H${c - 24} M${c + 24} 18 H${c + 47.4}`, class: "line" }),
|
|
el("circle", { cx: c, cy: 18, r: 24, class: "line" }),
|
|
el("circle", { cx: c + 52, cy: 18, r: 4.6, class: "line" }),
|
|
el("text", { x: c, y: 24.5, "text-anchor": "middle", class: "tag" }, "FW"));
|
|
return el("svg", { class: cls, viewBox: `-4 -4 ${W + 8} 176`, role: "img", "aria-label": "Housecarl" },
|
|
el("title", null, "Housecarl"), letterG, ruleG);
|
|
}
|
|
|
|
// logo returns the Housecarl logo (artwork/logo.svg, via scripts/logo.py)
|
|
// as an SVG of the given size in pixels: the full doorway, or with small
|
|
// the one without carvings (for sizes under about 64 pixels). Its color
|
|
// is the .logo-mark rule in style.css.
|
|
export function logo(size, { small = false, cls = "logo" } = {}) {
|
|
return el("svg", { class: cls, width: size, height: size, viewBox: logoData.viewBox, "aria-hidden": "true", focusable: "false" },
|
|
el("path", { d: small ? logoData.small : logoData.full, "fill-rule": "evenodd", class: "logo-mark" }));
|
|
}
|