diff --git a/CHANGELOG.md b/CHANGELOG.md index c929b2b..fc7f871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ### Changed +- Every panel page is laid out in one column of the same width, so moving + between them no longer shifts the navigation and the cards sideways. The + column used to be the 48rem reading measure, which the send log, the mail + queue and the system log widened to 64rem for their tables — and since the + navigation and the page are centred as a pair, that difference moved + everything on screen on the way between two pages. The column is now 64rem + throughout, with the reading measure kept inside it: a page's heading, cards, + back link and version footer are held to 48rem and centred in the column, + and the three pages made of data opt out and take the column whole. Which + pages those are is declared by the page itself (a `wide` block in its + template, the same mechanism as the section index) rather than derived from + the navigation entry, so a single delivery's page — prose, but filed under + the send log — keeps the measure. The scrollbar's width is now reserved on + every page as well: without it a short page and a long one were laid out in + viewports differing by that width, which moved the same things again. + - The mark's small-size variant — the tab icon and the `SP` initials it carries — sets its S in Medium where the wordmark sets it in ExtraLight. Against the P's SemiBold the ExtraLight S is a 0.90 stem against 3.40, diff --git a/internal/web/static/panel.css b/internal/web/static/panel.css index 09ccd9d..0e1f51f 100644 --- a/internal/web/static/panel.css +++ b/internal/web/static/panel.css @@ -46,6 +46,13 @@ } } * { box-sizing: border-box; } +/* The scrollbar's width is reserved whether or not the page is long enough to + need one. Without it a short page (Account) and a long one (Status) are laid + out in viewports differing by the scrollbar's width, and everything on the + page — the navigation column included — shifts sideways on the way between + them. The panel is a set of pages an operator moves through, so that shift + read as the layout jumping. */ +html { scrollbar-gutter: stable; } body { font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; padding: 2rem 1rem; background: var(--bg); color: var(--fg); @@ -54,13 +61,37 @@ body { pair. The navigation is a fixed width and the page keeps the measure it had: the column is added beside the content, not taken out of it. */ .shell { display: flex; justify-content: center; align-items: flex-start; gap: 1.75rem; } +/* One column for every page. It used to be the reading measure, 48rem, which + the three pages holding data widened to 64rem — and since the shell centres + the navigation and the page as a pair, that difference moved the navigation + column and the left edge of every card on the way between two pages. The + column is now the wider of the two everywhere, so nothing moves; the reading + measure is kept inside it by the rule below rather than by the column. + No auto margins: inside the shell they would eat the free space and push the + navigation column off to the far edge, so the centring is the shell's job + (justify-content above). */ +main { flex: 1 1 auto; min-width: 0; width: 100%; max-width: 64rem; } /* 48rem is a reading measure: right for the forms and prose that make up most - of the panel. Page-specific overrides below widen or narrow it for the pages - that need something else. No auto margins: inside the shell they would eat - the free space and push the navigation column off to the far edge, so the - centring is the shell's job (justify-content above). */ -main { flex: 1 1 auto; min-width: 0; width: 100%; max-width: 48rem; } -h1 { font-size: 1.4rem; margin: 0 0 1rem; } + of the panel, and too narrow for the pages that are tables of data. Every + page therefore keeps its content at that measure, centred in the column, and + the pages made of data opt out (main.wide below) and take the column whole. + The cap is on
's children rather than on .card so a page's heading, its + back link and the version footer travel with the cards: capping the cards + alone would leave the heading spanning the column with the card it belongs to + floating in the middle — the misalignment that gave login and setup their own + narrow column (main.page-login further down). */ +main > * { max-width: 48rem; margin-left: auto; margin-right: auto; } +/* The three monitoring pages are the ones that opt out: six columns of send + log, and raw mail.log lines that are long by nature, which at the measure + left Subject and Status fighting over the last inch and wrapped every second + log line. The class is declared by the page itself (the "wide" block in + layout.html) rather than derived from the page name, since a delivery's own + page is prose and shares the send log's name in the navigation. */ +main.wide > * { max-width: none; } +/* Longhands, not the margin shorthand: the shorthand would re-zero the auto + side margins above and pin the heading to the column's edge, a measure's + worth to the left of the card under it. */ +h1 { font-size: 1.4rem; margin-top: 0; margin-bottom: 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 @@ -100,13 +131,6 @@ button:hover, a.btn:hover { background: #1d4ed8; } /* form.inline only exists so a POST can sit next to other content without a form's block layout; its button is styled like any other. */ form.inline { display: inline; margin: 0; } -/* The three monitoring pages hold data instead of prose — seven columns of - send-log, and raw mail.log lines that are long by nature — and at the - default measure the send-log's Subject and Status were fighting over the - last inch while the log pages wrapped every second line. They get a wider - measure; every other page keeps the default 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 @@ -141,7 +165,13 @@ td.subject span { .code { display: block; white-space: pre-wrap; word-break: break-all; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.85rem; background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.7rem 0.8rem; margin: 0.3rem 0 0; } h2 { font-size: 1.05rem; margin: 0 0 0.4rem; } -.back { display: inline-block; margin-bottom: 1rem; } +/* The back link is a block so it takes the measure and lines up with the + heading above it and the card below: auto side margins centre a block, but + they do nothing for an inline-block, which would have stayed at the column's + edge while everything around it moved in. Its own text stays at the left of + that block — only the click target grows to the line, as it does for the + navigation entries. */ +.back { display: block; margin-bottom: 1rem; } /* Build version, closing every authenticated page. Quiet on purpose: it is reference material, not something to read on the way past. */ .version { margin-top: 1.6rem; text-align: right; font-size: 0.8rem; color: #6b7280; } @@ -300,7 +330,10 @@ button.danger:hover, a.danger:hover { background: #912018; } /* Jumping to a card should not leave it touching the top edge of the window. */ .card[id] { scroll-margin-top: 1rem; } /* Below the width the two columns need (13.5rem of navigation, 1.75rem of gap - and the 48rem measure, plus the body's padding), the column lies back down + and the 48rem measure, plus the body's padding — the measure, not the column + it sits in: a page column short of its 64rem is only a page column that has + not reached its cap, while a measure that no longer fits is a page that has), + the column lies back down into a bar above the page — the same wrapping rows it used to be. A drawer behind a hamburger would save more height, but it would need script to open, and the panel's navigation is six entries: they fit. */ diff --git a/internal/web/templates/deliveries.html b/internal/web/templates/deliveries.html index a4b341b..6a510ff 100644 --- a/internal/web/templates/deliveries.html +++ b/internal/web/templates/deliveries.html @@ -1,3 +1,9 @@ +{{/* The send log is six columns of addresses, subjects and timestamps, so this + page takes the whole column rather than the reading measure (see the "wide" + block in layout.html). A single delivery's page carries prose and keeps the + measure, which is why this is declared per page and not per page name. */}} +{{define "wide"}}wide{{end}} + {{define "content"}}

Deliveries

diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index fe04ed3..83676bf 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -28,11 +28,14 @@
{{if .User}}{{template "nav" .}}{{end}} {{/* The page name travels onto
as a class so the stylesheet can size a - page to what it holds — the monitoring pages are wider than the panel's - form pages (see main.page-* in panel.css). It is .Active, already carried - for the navigation, rather than a second field every handler would have to - remember to set. */}} -
+ page to what it holds — the signed-out pages are narrower than the rest + (see main.page-* in panel.css). It is .Active, already carried for the + navigation, rather than a second field every handler would have to + remember to set. The "wide" block beside it is how a page made of data + asks for the whole column instead of the reading measure; it is the page's + own declaration rather than a name in the stylesheet, since .Active cannot + tell the send log from a single delivery's page. */}} +
{{template "content" .}} {{/* The running version, on every authenticated page: it is what a backup manifest is checked against on restore and the first thing to establish @@ -85,6 +88,14 @@ nothing keeps it and renders no index. */}} {{define "sections"}}{{end}} +{{/* Whether the page takes the column whole. Empty here, so a page is held to + the reading measure unless it says otherwise; the three pages that are + tables of data or raw log lines redefine this as the word "wide", which + lands in
's class list (see main.wide in panel.css). Same mechanism + as "sections" above: each page's template file is parsed after this one, + so its definition replaces the empty one. */}} +{{define "wide"}}{{end}} + {{/* Navigation icons. Inline SVG rather than an icon font or sprite file: they inherit the link's colour through currentColor, cost no extra request, and need no exemption from the panel's "default-src 'self'" policy. Each is diff --git a/internal/web/templates/mail_queue.html b/internal/web/templates/mail_queue.html index 39ba7cf..f9dba13 100644 --- a/internal/web/templates/mail_queue.html +++ b/internal/web/templates/mail_queue.html @@ -1,3 +1,7 @@ +{{/* A table of queue ids, sizes, senders and recipients: the whole column + rather than the reading measure (see the "wide" block in layout.html). */}} +{{define "wide"}}wide{{end}} + {{define "content"}}

Mail queue

diff --git a/internal/web/templates/system_log.html b/internal/web/templates/system_log.html index fec9be9..1bbf142 100644 --- a/internal/web/templates/system_log.html +++ b/internal/web/templates/system_log.html @@ -1,3 +1,7 @@ +{{/* Raw mail.log lines, long by nature: the whole column rather than the + reading measure (see the "wide" block in layout.html). */}} +{{define "wide"}}wide{{end}} + {{define "content"}}

System log

diff --git a/internal/web/templates_test.go b/internal/web/templates_test.go index f167faf..8e2bbbd 100644 --- a/internal/web/templates_test.go +++ b/internal/web/templates_test.go @@ -215,6 +215,32 @@ func TestNavLeadsWithStatusAndPointsDomainsAtItsOwnPath(t *testing.T) { } } +// Whether a page takes the whole column or the reading measure is declared by +// the page's own "wide" block (see layout.html), which the layout stamps into +//
's class list. A page that loses the block does not fail to render — it +// silently comes back at the measure, with its table squeezed into two thirds +// of the column — so the set is asserted here, in both directions. +func TestOnlyThePagesMadeOfDataDeclareThemselvesWide(t *testing.T) { + tmpl, err := loadTemplates() + if err != nil { + t.Fatalf("loadTemplates: %v", err) + } + wide := map[string]bool{"deliveries": true, "mail_queue": true, "system_log": true} + for name, page := range tmpl.pages { + var buf bytes.Buffer + if err := page.ExecuteTemplate(&buf, "wide", nil); err != nil { + t.Fatalf("execute the wide block of %s: %v", name, err) + } + got := strings.TrimSpace(buf.String()) + switch { + case wide[name] && got != "wide": + t.Errorf("page %q no longer declares itself wide (%q); its data falls back to the reading measure", name, got) + case !wide[name] && got != "": + t.Errorf("page %q declares itself %q; only the pages that are tables of data or raw log lines take the whole column", name, got) + } + } +} + // Since the panel root redirects to the status page, a link left pointing at // "/" silently lands on the wrong screen instead of failing — so no template may // contain one.