From 827fea6ea887adaae5d4db9d4f5c74eeb62c3d49 Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Tue, 4 Aug 2026 19:24:48 +0300 Subject: [PATCH] brand: put the mark on the README and in the panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stamp existed as files and appeared nowhere. It now opens the README, sits at the left of the panel's navigation bar in its compact form, and stands above the card on the two pages that carry no navigation — sign-in and first-run setup, where it is the only thing telling an administrator which service just asked them for a password. The tab icon moves to the stamp's small-size variant, so the tab, the panel and the README stop being two different identities; the earlier envelope drawing stays in docs/assets/icon.png, unreferenced. The panel serves its own copies under internal/web/static/ because go:embed cannot reach outside the package; the proof sheet says which file feeds which slot. favicon.png is regenerated from the same icon at 32x32 for browsers with no SVG favicon support, and is declared before the SVG so those that do take the vector. Sign-in and setup also get a column the width of their own card. Both are a single .card.narrow, which centres itself while the heading above it stays at the panel's left edge — with the mark added that would have been three alignments on a page with four elements. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 19 ++++++++++++++++ README.md | 2 ++ internal/web/handlers_auth.go | 9 ++++++-- internal/web/handlers_setup.go | 7 +++--- internal/web/static/favicon.png | Bin 5129 -> 1654 bytes internal/web/static/favicon.svg | 8 +++++++ internal/web/static/logo-compact.svg | 8 +++++++ internal/web/static/logo.svg | 10 +++++++++ internal/web/static/panel.css | 28 +++++++++++++++++++++++- internal/web/templates/layout.html | 31 ++++++++++++++++++--------- internal/web/templates/login.html | 4 ++++ internal/web/templates/setup.html | 1 + 12 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 internal/web/static/favicon.svg create mode 100644 internal/web/static/logo-compact.svg create mode 100644 internal/web/static/logo.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 253309d..32376fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,27 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +### Added + +- The project's mark is now in use rather than only on file. The README opens + with the full stamp; the panel carries the compact one at the left of its + navigation bar, linking to the status page, and the full one above the card + on the two pages that have no navigation — sign-in and first-run setup. The + browser tab icon changes with it, from the earlier envelope drawing to the + stamp's small-size variant, so the tab, the panel and the README are one + identity. The four brand files in `docs/assets/` had their wordmark converted + from live text to outlines: they were set in IBM Plex Sans, which is not + installed on the machines that render them, and the light/semibold contrast + between *Self* and *Post* — the whole of the mark — collapsed into whatever + fallback the viewer happened to have. + ### Changed +- panel: the sign-in and setup pages are now a column the width of their own + card. Both are a single narrow card, which centred itself while the heading + above it stayed at the panel's left edge; adding the mark would have made + that three alignments on a page with four elements. + - panel: the three monitoring pages — Deliveries, Mail queue, System log — are now laid out wider (64rem against the 48rem the rest of the panel keeps). They carry data rather than prose: the send-log's seven columns had no room diff --git a/README.md b/README.md index cccca31..d09ddd3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![SelfPost](docs/assets/selfpost-stamp.svg) + # SelfPost Self-hosted outbound SMTP relay with a web control panel, shipped as a single diff --git a/internal/web/handlers_auth.go b/internal/web/handlers_auth.go index 0682e26..569c99a 100644 --- a/internal/web/handlers_auth.go +++ b/internal/web/handlers_auth.go @@ -98,6 +98,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { // a failing form. s.render(w, http.StatusOK, "login", map[string]any{ "Title": "SelfPost — Sign in", + "Active": "login", "SetupHint": true, }) return @@ -115,9 +116,13 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { } func (s *Server) renderLogin(w http.ResponseWriter, status int, formErr string) { + // Active names the page for the layout even though no navigation is drawn + // here: it is what puts page-login on
, which the stylesheet uses to + // give the signed-out pages a column the width of their own card. s.render(w, status, "login", map[string]any{ - "Title": "SelfPost — Sign in", - "Error": formErr, + "Title": "SelfPost — Sign in", + "Active": "login", + "Error": formErr, }) } diff --git a/internal/web/handlers_setup.go b/internal/web/handlers_setup.go index 020aeeb..1505f75 100644 --- a/internal/web/handlers_setup.go +++ b/internal/web/handlers_setup.go @@ -42,9 +42,10 @@ func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request) { func (s *Server) renderSetupForm(w http.ResponseWriter, status int, token, formErr string) { s.render(w, status, "setup", map[string]any{ - "Title": "SelfPost — Create administrator", - "Token": token, - "Error": formErr, + "Title": "SelfPost — Create administrator", + "Active": "setup", + "Token": token, + "Error": formErr, }) } diff --git a/internal/web/static/favicon.png b/internal/web/static/favicon.png index aa319b812437b0ea5089133632eaf7cb5d59ad42..cd7cc89be8bfa886858abccf23139520c4f231d1 100644 GIT binary patch literal 1654 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 rt;JfKr{CIX``J`_9Ak|o3 + + + + + diff --git a/internal/web/static/logo-compact.svg b/internal/web/static/logo-compact.svg new file mode 100644 index 0000000..3c03928 --- /dev/null +++ b/internal/web/static/logo-compact.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/internal/web/static/logo.svg b/internal/web/static/logo.svg new file mode 100644 index 0000000..cb46ba3 --- /dev/null +++ b/internal/web/static/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/internal/web/static/panel.css b/internal/web/static/panel.css index b4d54ba..2cede26 100644 --- a/internal/web/static/panel.css +++ b/internal/web/static/panel.css @@ -17,6 +17,13 @@ body { } main { max-width: 42rem; margin: 0 auto; } h1 { font-size: 1.4rem; margin: 0 0 1rem; } +/* The full mark, on the two pages that have no navigation bar to carry the + compact one. It takes the column's width so its edges line up with the card + below it, capped at that column's own 24rem; height stays automatic because + the stamp's proportions are part of the mark and it is never scaled unevenly. + The subtitle stops resolving below 280px, which a 320px viewport still clears + once the body's padding is taken off. */ +.mark { display: block; width: 100%; max-width: 24rem; height: auto; margin-bottom: 1.4rem; } .card { background: #fff; border: 1px solid #e2e5e9; border-radius: 10px; padding: 1.5rem; margin: 0 auto; @@ -58,6 +65,13 @@ main { max-width: 48rem; } every second line. They get a wider measure; every other page keeps the narrow one. The class comes from the layout, which stamps the page name onto
. */ main.page-deliveries, main.page-mail_queue, main.page-system_log { max-width: 64rem; } +/* The signed-out pages are a single card and nothing else, and .card.narrow + centres itself inside whatever holds it — so at the panel's usual width the + card floated in the middle while the mark and the heading stayed at the far + left, three alignments on a page with four elements. Narrowing the column to + the card's own width makes the three line up and puts the block as a whole in + the middle of the page. */ +main.page-login, main.page-setup { max-width: 24rem; } .card + .card { margin-top: 1.2rem; } .flash { background: #ecfdf3; border: 1px solid #abefc6; color: #067647; padding: 0.7rem 1rem; border-radius: 8px; margin-bottom: 1.2rem; } @media (prefers-color-scheme: dark) { .flash { background: #0d2818 !important; border-color: #1a5336 !important; color: #75d99b !important; } } @@ -149,9 +163,21 @@ details form { margin-top: 0.6rem; } } @media (prefers-color-scheme: dark) { .nav { border-color: #2b3138 !important; } } .nav .links, .nav .session { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; align-items: center; } -/* Pushed to the far edge so the two rows read as two blocks rather than as one +/* The top row: mark at one edge, session block at the other. Splitting them to + the edges is what makes the two rows read as two blocks rather than as one ragged list that happened to wrap. */ +.nav .top { display: flex; align-items: center; justify-content: space-between; gap: 0.9rem; } .nav .session { justify-content: flex-end; } +/* The mark is a link, but not one of the bar's entries: it takes none of the + padding and rounding the entry rule below applies, so its own edge lines up + with the page rather than sitting half a step inside it. The stamp carries + about 4px of field inside the file at this size, which is what puts it level + with the icons of the row underneath. */ +.nav .brand { padding: 0; } +/* width/height are on the element too, so the row reserves the space before the + SVG has loaded; these keep the ratio if the box is ever squeezed. The stamp's + proportions are part of the mark — it is never scaled unevenly. */ +.nav .brand img { display: block; width: 110px; height: auto; } /* Each entry pairs an icon with its label, so the entry itself is a flex row rather than a run of text — that is also why the bar centres its items instead of aligning them on the text baseline. Account is included: it is a diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index b6ce836..5b3249c 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -4,7 +4,13 @@ {{.Title}} - +{{/* The tab icon is the stamp's small-size variant — the initials rather than + the two-line wordmark, which stops resolving below 32px (the thresholds are + recorded in docs/assets/selfpost-proof.html). The PNG is declared first and + the SVG second: a browser takes the last icon whose type it understands, so + vector wins wherever it is supported and the raster catches the rest. */}} + + {{/* Unless told otherwise, htmx injects a stylesheet element of its own into the head for the request-indicator classes. The panel uses no @@ -40,15 +46,20 @@ .User and get no nav. */}} {{define "nav"}}